eletrotupi / tcc/ frontend/app/triggers/[id].tsxc6faa4c901e5f6c4ca8395c08d412ed57e708f6a
12.9 KB | 440 lines Raw
1
import { useState, useEffect } from "react";
2
import {
3
  StyleSheet,
4
  View,
5
  ScrollView,
6
  Pressable,
7
  ActivityIndicator,
8
} from "react-native";
9
import { Text } from "@/components/ui/Text";
10
import { useLocalSearchParams, router, Stack } from "expo-router";
11

12
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
13
import { NoLinkMessage } from "@/components/misc/NoLinkMessage";
14
import { Badge } from "@/components/ui/Badge";
15
import { Button } from "@/components/ui/Button";
16
import { Col, Row } from "@/components/ui/LayoutHelpers";
17
import { Section, SectionHeader } from "@/components/ui/Sections";
18
import { Theme, Typography, Colors, Spacing } from "@/constants/theme";
19
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
20
import { getTriggerCategory } from "@/constants/trigger-categories";
21
import { getCareActionMeta } from "@/constants/care-action-categories";
22
import {
23
  useTrigger,
24
  useDeleteTrigger,
25
} from "@/hooks/useTrigger.queries";
26
import { usePatchCareAction } from "@/hooks";
27
import { useCareActionLinkStore } from "@/stores";
28
import { formatMoment } from "@/lib/utils/time";
29
import { getMood } from "@/constants/moods";
30

31
const impactColor = (n: number) => {
32
  if (n <= 1) return '#86efac';
33
  if (n <= 2) return '#22c55e';
34
  if (n <= 3) return '#f59e0b';
35
  if (n <= 4) return '#f97316';
36
  return '#ef4444';
37
};
38

39
export default function TriggerDetailsScreen() {
40
  const { id } = useLocalSearchParams<{ id: string }>();
41
  const { data: trigger, isLoading, isError } = useTrigger(id);
42
  const deleteTrigger = useDeleteTrigger();
43
  const patchCareAction = usePatchCareAction();
44
  const { pendingId, triggerLinked } = useCareActionLinkStore();
45
  const hasPendingCareAction = !!pendingId && !triggerLinked;
46
  const [minutesLeft, setMinutesLeft] = useState(0);
47
  const [canDelete, setCanDelete] = useState(false);
48

49
  useEffect(() => {
50
    const updateTimer = () => {
51
      if (!trigger?.createdAt) return;
52
      const createdAt = new Date(trigger.createdAt);
53

54
      const msSinceCreation = Date.now() - createdAt.getTime();
55
      const deletionWindowMs = 5 * 60 * 1000;
56

57
      const newCanDelete = msSinceCreation < deletionWindowMs;
58
      const newMinutesLeft = Math.max(
59
        0,
60
        Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
61
      );
62

63
      setCanDelete(newCanDelete);
64
      setMinutesLeft(newMinutesLeft);
65
    };
66

67
    if (isLoading) return;
68

69
    updateTimer(); // Set initial values
70

71
    const interval = setInterval(updateTimer, 1000);
72

73
    return () => clearInterval(interval);
74
  }, [trigger, isLoading]);
75

76
  if (isLoading) {
77
    return (
78
      <View style={styles.centered}>
79
        <ActivityIndicator size="large" color={Colors.light.tint} />
80
      </View>
81
    );
82
  }
83

84
  if (isError || !trigger) {
85
    return (
86
      <View style={styles.centered}>
87
        <Text style={styles.errorText}>Registro não encontrado.</Text>
88
        <Pressable onPress={() => router.back()} style={styles.backButton}>
89
          <Text style={styles.backButtonText}>Voltar</Text>
90
        </Pressable>
91
      </View>
92
    );
93
  }
94

95
  const meta = getTriggerCategory(trigger.category);
96

97
  const handleDelete = async () => {
98
    await deleteTrigger.mutateAsync(String(trigger.id));
99
    router.back();
100
  };
101

102
  const handleLinkCareAction = async () => {
103
    const careActionId = useCareActionLinkStore.getState().linkTrigger();
104
    if (!careActionId) return;
105
    await patchCareAction.mutateAsync({
106
      id: String(careActionId),
107
      data: { triggerId: trigger.id },
108
    });
109
  };
110

111
  const deleteButtonMsg = () => {
112
    const msg = "Excluir Registro";
113
    const timeLeftStr = `(Disp. por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
114
    const suffix = canDelete ? timeLeftStr : "(Expirado)";
115

116
    return `${msg} ${suffix}`;
117
  };
118

119
  const hasAnyLinks = (
120
    trigger.careActions && trigger.careActions.length > 0
121
  ) || (trigger.moodLinks && trigger.moodLinks.length > 0);
122

123
  return (
124
    <>
125
      <Stack.Screen options={{ title: "Ver detalhes" }} />
126

127
      <ScrollView
128
        style={styles.container}
129
        contentContainerStyle={styles.content}
130
        showsVerticalScrollIndicator={false}
131
      >
132
        {/* Header card */}
133
        <Card style={styles.headerCard}>
134
          <Row style={{ alignItems: "center" }}>
135
            <View style={[styles.iconWrap, { backgroundColor: meta.iconBackground }]}>
136
              <MaterialIcons name={meta.icon} size={22} color={meta.color} />
137
            </View>
138

139
            <Col gap={2}>
140
              <Text style={styles.triggerLabel}>
141
                {meta.label}
142
              </Text>
143
              <Text style={styles.triggerMoment}>
144
                {formatMoment(new Date(trigger.moment))}
145
              </Text>
146
            </Col>
147
          </Row>
148
        </Card>
149

150
        {/* Pending care action link banner */}
151
        {hasPendingCareAction && (
152
          <Pressable
153
            onPress={handleLinkCareAction}
154
            disabled={patchCareAction.isPending}
155
            style={({ pressed }) => [
156
              styles.linkBanner,
157
              pressed && { opacity: 0.75 },
158
            ]}
159
          >
160
            <View style={styles.linkBannerIcon}>
161
              <Ionicons name="link-outline" size={18} color="#1D4ED8" />
162
            </View>
163
            <Text style={styles.linkBannerText}>
164
              Vincular à consulta registrada
165
            </Text>
166
            <Ionicons name="chevron-forward" size={16} color="#1D4ED8" />
167
          </Pressable>
168
        )}
169

170
        {/* Linked moods */}
171
        <Section>
172
          <SectionHeader title="Vínculo" variant="subtle" />
173
          {!!hasAnyLinks ? (
174
            <Col gap={8}>
175
              {(trigger.moodLinks ?? []).map((link) => {
176
                const moodDef = getMood(link.mood.selectedMood.toLowerCase());
177
                return (
178
                  <Pressable
179
                    key={link.id}
180
                    onPress={() => router.push(`/entry/${link.mood.id}`)}
181
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
182
                  >
183
                    <Card style={styles.linkCard}>
184
                      <Row style={{ alignItems: "center", gap: 12 }}>
185
                        <View style={styles.linkIconCircle}>
186
                          <Text style={styles.linkMoodEmoji}>{moodDef?.icon ?? "😐"}</Text>
187
                        </View>
188
                        <Col gap={3} style={{ flex: 1 }}>
189
                          <Row style={{ alignItems: "center", gap: 6 }}>
190
                            <Text style={styles.linkLabel}>
191
                              {moodDef?.label ?? link.mood.selectedMood}
192
                            </Text>
193

194
                            <Badge
195
                              label="Humor"
196
                              variant="yellow"
197
                              style={undefined}
198
                            />
199
                          </Row>
200

201
                          <Text style={styles.linkTime}>
202
                            {formatMoment(new Date(link.mood.moment))}
203
                          </Text>
204
                        </Col>
205
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
206
                      </Row>
207
                      <View style={styles.impactBar}>
208
                        <View
209
                          style={[
210
                            styles.impactFill,
211
                            {
212
                              width: `${(link.perceivedImpact / 5) * 100}%`,
213
                              backgroundColor: impactColor(link.perceivedImpact),
214
                            },
215
                          ]}
216
                        />
217
                      </View>
218
                    </Card>
219
                  </Pressable>
220
                );
221
              })}
222

223
              {(trigger.careActions ?? []).map((ca) => {
224
                const meta = getCareActionMeta(ca);
225
                return (
226
                  <Pressable
227
                    key={ca.id}
228
                    onPress={() => router.push(`/care-actions/${ca.id}`)}
229
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
230
                  >
231
                    <Card style={styles.linkCard}>
232
                      <Row style={{ alignItems: 'center', gap: 12 }}>
233
                        <View style={[styles.linkIconCircle, { backgroundColor: meta.iconBackground }]}>
234
                          <MaterialIcons name={meta.icon} size={22} color={meta.color} />
235
                        </View>
236
                        <Col gap={3} style={{ flex: 1 }}>
237
                          <Row style={{ alignItems: 'center', gap: 6 }}>
238
                            <Text style={styles.linkLabel}>{meta.label}</Text>
239
                            <Badge label="Consulta" variant="blue" style={undefined} />
240
                          </Row>
241
                          <Text style={styles.linkTime}>
242
                            {formatMoment(new Date(ca.moment))}
243
                          </Text>
244
                        </Col>
245
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
246
                      </Row>
247
                    </Card>
248
                  </Pressable>
249
                );
250
              })}
251
            </Col>
252
          ) : (
253
            <NoLinkMessage />
254
          )}
255
        </Section>
256

257
        {/* Annotation / notes */}
258
        {trigger.comment && (
259
          <Section>
260
            <SectionHeader title="Anotações" variant="subtle" />
261
            <Card style={{ padding: Spacing.cardGap }}>
262
              <Text style={styles.annotation}>
263
                &quot;{trigger.comment?.trim()}&quot;
264
              </Text>
265
            </Card>
266
          </Section>
267
        )}
268

269
        {/* Delete */}
270
        <View style={styles.section}>
271
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
272
          <Button
273
            variant="outline"
274
            loading={false}
275
            disabled={!canDelete}
276
            onPress={() => router.push(`/triggers/edit/${trigger.id}`)}
277
            title="Editar Registro"
278
          />
279
          <Button
280
            variant="danger"
281
            loading={deleteTrigger.isPending}
282
            disabled={!canDelete || deleteTrigger.isPending}
283
            onPress={handleDelete}
284
            title={deleteButtonMsg()}
285
          />
286

287
          <SubtleInfoCard
288
            style={styles.deleteInfo}
289
            text={`
290
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
291
              após o envio para garantir a integridade do histórico emocional.
292
              `}
293
          />
294
        </View>
295
      </ScrollView>
296
    </>
297
  );
298
}
299

300
const styles = StyleSheet.create({
301
  container: {
302
    flex: 1,
303
    backgroundColor: Theme.colors.light.background,
304
  },
305
  content: {
306
    padding: Spacing.containerPadding,
307
    gap: Spacing.sectionGap,
308
    paddingBottom: 48,
309
  },
310
  iconWrap: {
311
    width: 40,
312
    height: 40,
313
    borderRadius: 20,
314
    alignItems: 'center',
315
    justifyContent: 'center',
316
    marginTop: 1
317
  },
318
  headerCardContent: {
319
    flexDirection: "row",
320
    alignItems: "center",
321
    gap: 8,
322
  },
323
  triggerLabel: {
324
    ...Typography.headlineLg,
325
    fontWeight: 700,
326
    color: Colors.light.text,
327
  },
328
  centered: {
329
    flex: 1,
330
    justifyContent: "center",
331
    alignItems: "center",
332
    gap: 12,
333
  },
334
  errorText: {
335
    fontSize: 15,
336
    color: Colors.light.textSecondary,
337
  },
338
  backButton: {
339
    paddingHorizontal: 20,
340
    paddingVertical: 10,
341
    backgroundColor: Theme.colors.light.tint,
342
    borderRadius: Theme.borderRadius.md,
343
  },
344
  backButtonText: {
345
    fontWeight: "700",
346
    color: Theme.colors.light.text,
347
  },
348

349
  // Pending link banner
350
  linkBanner: {
351
    flexDirection: 'row',
352
    alignItems: 'center',
353
    gap: 10,
354
    backgroundColor: '#DBEAFE',
355
    borderRadius: 10,
356
    padding: 12,
357
  },
358
  linkBannerIcon: {
359
    width: 32,
360
    height: 32,
361
    borderRadius: 16,
362
    backgroundColor: '#BFDBFE',
363
    alignItems: 'center',
364
    justifyContent: 'center',
365
  },
366
  linkBannerText: {
367
    flex: 1,
368
    fontSize: 14,
369
    fontWeight: '600',
370
    color: '#1D4ED8',
371
  },
372

373
  // Header
374
  headerCard: {
375
    padding: 16,
376
  },
377
  triggerMoment: {
378
    fontSize: 13,
379
    color: Colors.light.textSecondary,
380
  },
381

382
  // Sections
383
  section: {
384
    gap: 8,
385
  },
386
  sectionLabel: {
387
    fontSize: 11,
388
    fontWeight: "600",
389
    letterSpacing: 0.8,
390
    color: Colors.light.textSecondary,
391
    textTransform: "uppercase",
392
  },
393

394
  // Annotation
395
  annotation: {
396
    fontSize: 14,
397
    lineHeight: 22,
398
    color: Colors.light.textSecondary,
399
    fontStyle: "italic",
400
    padding: 4,
401
  },
402
  deleteInfo: {
403
    marginTop: 4,
404
  },
405
  // Linked mood cards
406
  linkCard: {
407
    padding: Spacing.cardGap,
408
    overflow: "hidden",
409
    gap: 10,
410
  },
411
  linkIconCircle: {
412
    width: 44,
413
    height: 44,
414
    alignItems: "center",
415
    justifyContent: "center",
416
    borderRadius: 20,
417
  },
418
  linkMoodEmoji: {
419
    fontSize: 28,
420
  },
421
  linkTime: {
422
    fontSize: 12,
423
    color: Colors.light.textSecondary,
424
  },
425
  linkLabel: {
426
    fontSize: 15,
427
    fontWeight: "700",
428
    color: Colors.light.text,
429
  },
430
  impactBar: {
431
    height: 3,
432
    backgroundColor: Colors.light.divider,
433
    borderRadius: 99,
434
    overflow: "hidden",
435
  },
436
  impactFill: {
437
    height: "100%",
438
    borderRadius: 99,
439
  },
440
});